package com.sky.framework.json;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.time.temporal.Temporal;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.sky.framework.json.MatcherBehavior.CLASS_FIRST;
import static com.sky.framework.json.MatcherBehavior.PATH_FIRST;
import static java.util.Arrays.asList;

public class JsonViewSerializer extends JsonSerializer<JsonView> {
    public static boolean log = false;
    /**
     * Cached results from expensive (pure) methods
     */
    private final Memoizer memoizer;

    /**
     * Map of custom serializers to take into account when serializing fields.
     */
    private Map<Class<?>, JsonSerializer<Object>> customSerializersMap = null;

    private MatcherBehavior defaultMatcherBehavior = CLASS_FIRST;

    public JsonViewSerializer() {
        this(1024);
    }

    public JsonViewSerializer(int maxCacheSize) {
        this.memoizer = new Memoizer(maxCacheSize);
    }

    /**
     * Registering custom serializer allows to the JSonView to deal with custom serializations for certains field types.<br>
     * This way you could register for instance a JODA serialization as  a DateTimeSerializer. <br>
     * Thus, when JSonView find a field of that type (DateTime), it will delegate the serialization to the serializer specified.<br>
     * Example:<br>
     * <code>
     * JsonViewSupportFactoryBean bean = new JsonViewSupportFactoryBean( mapper );
     * bean.registerCustomSerializer( DateTime.class, new DateTimeSerializer() );
     * </code>
     *
     * @param <T>     Type class of the serializer
     * @param cls     {@link Class} The class type you want to add a custom serializer
     * @param forType {@link JsonSerializer} The serializer you want to apply for that type
     */
    @SuppressWarnings("unchecked")
    public <T> void registerCustomSerializer(Class<T> cls, JsonSerializer<T> forType) {
        if (customSerializersMap == null) {
            customSerializersMap = new HashMap<>();
        }

        if (cls == null) {
            throw new IllegalArgumentException("Class must not be null");
        } else if (cls.equals(JsonView.class)) {
            throw new IllegalArgumentException("Class cannot be " + JsonView.class);
        } else if (customSerializersMap.containsKey(cls)) {
            throw new IllegalArgumentException("Class " + cls + " already has a serializer registered (" + customSerializersMap.get(cls) + ")");
        }

        customSerializersMap.put(cls, (JsonSerializer<Object>) forType);
    }

    /**
     * Unregister a previously registered serializer. @see registerCustomSerializer
     *
     * @param cls The class type the serializer was registered for
     */
    public void unregisterCustomSerializer(Class<?> cls) {
        if (customSerializersMap != null) {
            customSerializersMap.remove(cls);
        }
    }

    /**
     * Set the default matcher behavior to be used if the {@link JsonView} object to
     * be serialized does not specify one.
     *
     * @param defaultMatcherBehavior The default behavior to use
     */
    public void setDefaultMatcherBehavior(MatcherBehavior defaultMatcherBehavior) {
        this.defaultMatcherBehavior = defaultMatcherBehavior;
    }

    @Override
    public void serialize(JsonView result, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
        new JsonWriter(serializers, jgen, result).write(null, result.getValue());
    }

    private static class MatchPrefixTuple {
        private final Match match;
        private final String prefix;

        public MatchPrefixTuple(Match match, String prefix) {
            this.match = match;
            this.prefix = prefix;
        }
    }

    static class AccessibleProperty {
        public final Class declaringClass;
        public final String name;
        public final Class type;
        public final Annotation[] annotations;
        public final int modifiers;
        public final Object property;
        private final Function<Object, Object> getter;

        public AccessibleProperty(String name, Annotation[] annotations, Object property) {
            this.name = name;
            this.annotations = annotations;
            this.property = property;

            if (property instanceof Field) {
                this.declaringClass = ((Field) property).getDeclaringClass();
                this.type = ((Field) property).getType();
                this.modifiers = ((Field) property).getModifiers();
                this.getter = this::getFromField;
            } else if (property instanceof Method) {
                this.declaringClass = ((Method) property).getDeclaringClass();
                this.type = ((Method) property).getReturnType();
                this.modifiers = ((Method) property).getModifiers();
                this.getter = this::getFromMethod;
            } else {
                throw new RuntimeException("Unable to access property from " + property);
            }
        }

        public Object get(Object obj) {
            return getter.apply(obj);
        }

        private Object getFromField(Object obj) {
            try {
                ((Field) property).setAccessible(true);
                return ((Field) property).get(obj);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        private Object getFromMethod(Object obj) {
            try {
                ((Method) property).setAccessible(true);
                return ((Method) property).invoke(obj);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            AccessibleProperty that = (AccessibleProperty) o;
            return Objects.equals(declaringClass, that.declaringClass) &&
                    Objects.equals(name, that.name);
        }

        @Override
        public int hashCode() {

            return Objects.hash(declaringClass, name);
        }
    }

    class JsonWriter {
        final SerializerProvider serializerProvider;
        final JsonGenerator jgen;
        final JsonView result;
        Stack<String> path = new Stack<>();
        String currentPath = "";
        Match currentMatch = null;
        AccessibleProperty referringField = null;

        JsonWriter(SerializerProvider serializerProvider, JsonGenerator jgen, JsonView result) {
            this.serializerProvider = serializerProvider;
            this.jgen = jgen;
            this.result = result;
        }

        //internal use only to encapsulate what the current state was
        private JsonWriter(JsonGenerator jgen, JsonView result, Match currentMatch, SerializerProvider serializerProvider) {
            this.jgen = jgen;
            this.result = result;
            this.currentMatch = currentMatch;
            this.serializerProvider = serializerProvider;
        }

        //internal use only to encapsulate what the current state was
        private JsonWriter(JsonGenerator jgen, JsonView result, Match currentMatch,
                           String currentPath, Stack<String> path, AccessibleProperty referringField, SerializerProvider serializerProvider) {
            this.jgen = jgen;
            this.result = result;
            this.currentMatch = currentMatch;
            this.currentPath = currentPath;
            this.referringField = referringField;
            this.path = path;
            this.serializerProvider = serializerProvider;
        }

        boolean writePrimitive(Object obj) throws IOException {
            if (obj instanceof String) {
                jgen.writeString((String) obj);
            } else if (obj instanceof Integer) {
                jgen.writeNumber((Integer) obj);
            } else if (obj instanceof Long) {
                jgen.writeNumber((Long) obj);
            } else if (obj instanceof Short) {
                jgen.writeNumber((Short) obj);
            } else if (obj instanceof Double) {
                jgen.writeNumber((Double) obj);
            } else if (obj instanceof Float) {
                jgen.writeNumber((Float) obj);
            } else if (obj instanceof Character) {
                jgen.writeNumber((Character) obj);
            } else if (obj instanceof Byte) {
                jgen.writeNumber((Byte) obj);
            } else if (obj instanceof Boolean) {
                jgen.writeBoolean((Boolean) obj);
            } else if (obj == null) {
                jgen.writeNull();
            } else if (obj instanceof BigDecimal) {
                jgen.writeNumber((BigDecimal) obj);
            } else {
                return false;
            }
            return true;
        }

        boolean writeSpecial(Object obj) throws IOException {
            if (obj instanceof Date) {
                serializerProvider.defaultSerializeDateValue((Date) obj, jgen);
            } else if (obj instanceof Temporal) {
                serializerProvider.defaultSerializeValue(obj, jgen);
            } else if (obj instanceof URL) {
                jgen.writeString(obj.toString());
            } else if (obj instanceof URI) {
                jgen.writeString(obj.toString());
            } else if (obj instanceof UUID) {
                jgen.writeString(obj.toString());
            } else if (obj instanceof Class) {
                jgen.writeString(((Class) obj).getCanonicalName());
            } else {
                return false;
            }
            return true;
        }

        boolean writeEnum(Object obj) throws IOException {
            if (obj.getClass().isEnum()) {
                jgen.writeString(((Enum) obj).name());
            } else {
                return false;
            }
            return true;
        }

        @SuppressWarnings("unchecked")
        boolean writeList(Object obj) throws IOException {
            if (obj instanceof List || obj instanceof Set || obj.getClass().isArray()) {
                Iterable iter;
                if (obj.getClass().isArray()) {
                    if (obj instanceof byte[]) {
                        jgen.writeBinary((byte[]) obj);
                        return true;
                    } else {
                        iter = convertArray(obj);
                    }
                } else {
                    iter = (Iterable<Object>) obj;
                }

                jgen.writeStartArray();
                for (Object o : iter) {
                    new JsonWriter(jgen, result, currentMatch, currentPath, path, referringField, serializerProvider).write(null, o);
                }
                jgen.writeEndArray();
            } else {
                return false;
            }
            return true;
        }

        @SuppressWarnings("unchecked")
        Iterable convertArray(Object obj) {
            Iterable iter;
            if (obj instanceof int[]) {
                int[] arr = (int[]) obj;
                iter = new ArrayList<>();
                for (int v : arr) {
                    ((List<Integer>) iter).add(v);
                }
            } else if (obj instanceof double[]) {
                double[] arr = (double[]) obj;
                iter = new ArrayList<>();
                for (double v : arr) {
                    ((List<Double>) iter).add(v);
                }
            } else if (obj instanceof float[]) {
                float[] arr = (float[]) obj;
                iter = new ArrayList<>();
                for (float v : arr) {
                    ((List<Float>) iter).add(v);
                }
            } else if (obj instanceof long[]) {
                long[] arr = (long[]) obj;
                iter = new ArrayList<>();
                for (long v : arr) {
                    ((List<Long>) iter).add(v);
                }
            } else if (obj instanceof short[]) {
                short[] arr = (short[]) obj;
                iter = new ArrayList<>();
                for (short v : arr) {
                    ((List<Short>) iter).add(v);
                }
            } else if (obj instanceof char[]) {
                char[] arr = (char[]) obj;
                iter = new ArrayList<>();
                for (char v : arr) {
                    ((List<Character>) iter).add(v);
                }
            } else if (obj instanceof boolean[]) {
                boolean[] arr = (boolean[]) obj;
                iter = new ArrayList<>();
                for (boolean v : arr) {
                    ((List<Boolean>) iter).add(v);
                }
            } else {
                iter = asList((Object[]) obj);
            }
            return iter;
        }

        @SuppressWarnings("unchecked")
        boolean writeMap(Object obj) throws IOException {
            if (obj instanceof Map) {
                Map<Object, Object> map = (Map<Object, Object>) obj;

                jgen.writeStartObject();
                for (Object key : map.keySet()) {
                    jgen.writeFieldName(key.toString());
                    new JsonWriter(jgen, result, currentMatch, serializerProvider).write(null, map.get(key));
                }
                jgen.writeEndObject();
            } else {
                return false;
            }
            return true;
        }

        @SuppressWarnings("unchecked")
        void writeObject(Object obj) throws IOException {
            jgen.writeStartObject();

            List<AccessibleProperty> fields = getAccessibleProperties(obj.getClass());

            // 扩展
            MatchPrefixTuple tuple = getMatchPrefix(obj.getClass());
            FieldExpandInterface fieldExpandInterface = null;
            if (null != tuple.match) {
                fieldExpandInterface = tuple.match.getFieldExpandInterface();
            }

            for (AccessibleProperty property : fields) {
                try {
                    //if the field has a serializer annotation on it, serialize with it
                    if (fieldAllowed(property, obj.getClass())) {
                        Object val = readField(obj, property);
                        if (!valueAllowed(property, val, obj.getClass())) {
                            continue;
                        }

                        // 扩展
                        String name = getFieldName(property);
                        if (null != fieldExpandInterface) {
                            boolean skip = fieldExpandInterface.filter(currentPath, name, val, jgen);
                            if (skip) {
                                continue;
                            }
                        }

                        jgen.writeFieldName(name);

                        JsonSerializer fieldSerializer = annotatedWithJsonSerialize(property);
                        if (fieldSerializer != null) {
                            fieldSerializer.serialize(val, jgen, serializerProvider);
                        } else if (customSerializersMap != null && val != null) {
                            JsonSerializer<Object> serializer = customSerializersMap.get(val.getClass());
                            if (serializer != null) {
                                serializer.serialize(val, jgen, serializerProvider);
                            } else {
                                new JsonWriter(jgen, result, currentMatch, currentPath, path, property, serializerProvider).write(name, val);
                            }
                        } else if (val instanceof JsonNode) {
                            // Let Jackson deal with these, they're special
                            serializerProvider.defaultSerializeValue(val, jgen);
                        } else {
                            new JsonWriter(jgen, result, currentMatch, currentPath, path, property, serializerProvider).write(name, val);
                        }

                        // 字段强化操作
                        if (null != fieldExpandInterface) {
                            fieldExpandInterface.fieldIntensify(currentPath, name, val, jgen);
                        }
                    }
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }

            // 扩展
            if (null != fieldExpandInterface) {
                fieldExpandInterface.append(currentPath, path, jgen);
            }

            jgen.writeEndObject();
        }

        boolean valueAllowed(AccessibleProperty property, Object value, Class cls) {
            Include defaultInclude = serializerProvider.getConfig() == null ? Include.ALWAYS : serializerProvider.getConfig().getSerializationInclusion();
            JsonInclude jsonInclude = getAnnotation(property, JsonInclude.class);
            JsonSerialize jsonSerialize = getAnnotation(cls, JsonSerialize.class);

            // Make sure local annotations win over global ones
            if (jsonInclude != null && jsonInclude.value() == Include.NON_NULL && value == null) {
                return false;
            }

            return value != null
                    || (defaultInclude == Include.ALWAYS && jsonSerialize == null)
                    || (jsonSerialize != null && jsonSerialize.include() == Inclusion.ALWAYS);
        }

        /**
         * Do a search for *all* matchers for a class. This takes into account all relevant
         * parents in the class hierarchy. If multiple matches are found, the matches will
         * be unioned together.
         */
        @SuppressWarnings("unchecked")
        private Optional<Match> classMatchSearch(Class declaringClass) {
//      return memoizer.classMatches(result, declaringClass, () -> {
            List<Match> matches = new ArrayList<>();
            Stack<Class> classes = new Stack<>();
            classes.push(declaringClass);
            while (!classes.isEmpty()) {
                Class cls = classes.pop();
                Match match = result.getMatch(cls);

                if (match != null) {
                    matches.add(match);
                }
                if (cls.getInterfaces() != null) {
                    Stream.of(cls.getInterfaces()).forEach(c -> classes.push(c));
                }
                if (cls.getSuperclass() != null && !cls.getSuperclass().equals(Object.class)) {
                    classes.push(cls.getSuperclass());
                }
            }

            if (matches.size() == 1) {
                return Optional.of(matches.get(0));
            } else if (matches.size() > 1) {
                // Join all the includes and excludes
                Match unionMatch = new Match();
                matches.forEach(m -> {
                    unionMatch.getExcludes().addAll(m.getExcludes());
                    unionMatch.getIncludes().addAll(m.getIncludes());
                    unionMatch.getTransforms().putAll(m.getTransforms());
                });
                return Optional.of(unionMatch);
            } else {
                return Optional.empty();
            }
//      });
        }

        @SuppressWarnings("unchecked")
        boolean fieldAllowed(AccessibleProperty property, Class declaringClass) {
            String name = property.name;
            if (Modifier.isStatic(property.modifiers)) {
                return false;
            }

            MatchPrefixTuple tuple = getMatchPrefix(declaringClass);
            String prefix = tuple.prefix;
            Match match = tuple.match;

            //if there is a match, respect it
            if (match != null) {

                if (currentMatch == null) {
                    currentMatch = match;
                }

                int included = containsMatchingPattern(match.getIncludes(), prefix + name, true);
                int excluded = containsMatchingPattern(match.getExcludes(), prefix + name, false);

        /*
        The logic for this is a little complex. We're dealing with ternary logic to
        properly handle wildcard matches. We want matches made with wildcards to be
        overruled by matches without them.
         */
                if (included == 1) {
                    return true;
                } else if (excluded == 1) {
                    return false;
                } else if (included == 0) {
                    return true;
                } else if (excluded == 0) {
                    return false;
                } else {
                    return !annotatedWithIgnore(property);
                }
            } else {
                //else, respect JsonIgnore only
                return !annotatedWithIgnore(property);
            }
        }

        MatchPrefixTuple getMatchPrefix(Class declaringClass) {
            String prefix = currentPath.length() > 0 ? currentPath + "." : "";

            // Determine matcher behavior
            MatcherBehavior currentBehavior = result.matcherBehavior;
            if (currentBehavior == null) {
                currentBehavior = JsonViewSerializer.this.defaultMatcherBehavior;
            }

            //search for matching class
            Match match = null;
            if (currentBehavior == CLASS_FIRST) {
                match = classMatchSearch(declaringClass).orElse(null);
                if (match == null) {
                    match = currentMatch;
                } else {
                    prefix = "";
                }
            } else if (currentBehavior == PATH_FIRST) {
                if (currentMatch != null) {
                    match = currentMatch;
                } else {
                    match = classMatchSearch(declaringClass).orElse(null);
                    prefix = "";
                }
            }

            return new MatchPrefixTuple(match, prefix);
        }

        Object readField(Object obj, AccessibleProperty field) throws IllegalAccessException {
            MatchPrefixTuple tuple = getMatchPrefix(obj.getClass());
            if (tuple.match != null && tuple.match.getTransforms().containsKey(tuple.prefix + field.name)) {
                return tuple.match.getTransforms().get(tuple.prefix + field.name).apply(obj, field.get(obj));
            } else {
                return field.get(obj);
            }
        }

        void write(String fieldName, Object value) throws IOException {
            if (fieldName != null) {
                path.push(fieldName);
                updateCurrentPath();
            }

            //try to handle all primitives/special cases before treating this as json object
            if (!writePrimitive(value) && !writeSpecial(value) && !writeEnum(value) && !writeList(value) && !writeMap(value)) {
                writeObject(value);
            }

            if (fieldName != null) {
                path.pop();
                updateCurrentPath();
            }
        }

        void updateCurrentPath() {
            StringBuilder builder = new StringBuilder();
            for (String s : path) {
                builder.append(".");
                builder.append(s);
            }
            currentPath = builder.length() > 0 ? builder.toString().substring(1) : "";
        }

        @SuppressWarnings("unchecked")
        <E> E readClassAnnotation(Class cls, Class annotationType, String methodName) {
            try {
                for (Annotation an : getAnnotations(cls)) {
                    Class<? extends Annotation> type = an.annotationType();
                    if (an.annotationType().equals(annotationType)) {
                        for (Method method : type.getDeclaredMethods()) {
                            if (method.getName().equals(methodName)) {
                                return (E) method.invoke(an, (Object[]) null);
                            }
                        }
                        throw new IllegalArgumentException("Method " + methodName + " not found on annotation " + annotationType);
                    }
                }
                throw new IllegalArgumentException("Annotation " + annotationType + " not found on class " + cls);
            } catch (InvocationTargetException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Returns one of the following values:
         * <pre>
         * -1: No match found
         *  0: Wildcard-based match
         *  1: Non-wildcard match
         * </pre>
         * <p>
         * This method is memoized to speed up execution time
         */
        int containsMatchingPattern(Set<String> values, String pattern, boolean matchPrefix) {
            return memoizer.matches(values, pattern, matchPrefix, () -> {
                int match = -1;
                for (String val : values) {
                    String replaced = val.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*");
                    if (Pattern.compile(replaced).matcher(pattern).matches() || (matchPrefix && val.startsWith(pattern + "."))) {
                        match = replaced.contains("*") ? 0 : 1;
                        break;
                    }
                }
                return match;
            });
        }

        /**
         * Returns a boolean indicating whether the provided field is annotated with
         * some form of ignore. This method is memoized to speed up execution time
         */
        boolean annotatedWithIgnore(AccessibleProperty f) {
            return memoizer.annotatedWithIgnore(f, () -> {
                JsonIgnore jsonIgnore = getAnnotation(f, JsonIgnore.class);
                JsonIgnoreProperties classIgnoreProperties = getAnnotation(f.declaringClass, JsonIgnoreProperties.class);
                JsonIgnoreProperties fieldIgnoreProperties = null;
                boolean backReferenced = false;

                //make sure the referring field didn't specify properties to ignore
                if (referringField != null) {
                    fieldIgnoreProperties = getAnnotation(referringField, JsonIgnoreProperties.class);
                }

                //make sure the referring field didn't specify a backreference annotation
                if (getAnnotation(f, JsonBackReference.class) != null && referringField != null) {
                    for (AccessibleProperty lastField : getAccessibleProperties(referringField.declaringClass)) {
                        JsonManagedReference fieldManagedReference = getAnnotation(lastField, JsonManagedReference.class);
                        if (fieldManagedReference != null && lastField.type.equals(f.declaringClass)) {
                            backReferenced = true;
                            break;
                        }
                    }
                }

                return (jsonIgnore != null && jsonIgnore.value()) ||
                        (classIgnoreProperties != null && asList(classIgnoreProperties.value()).contains(f.name)) ||
                        (fieldIgnoreProperties != null && asList(fieldIgnoreProperties.value()).contains(f.name)) ||
                        backReferenced;
            });
        }

        JsonSerializer annotatedWithJsonSerialize(AccessibleProperty property) {
            JsonSerialize jsonSerialize = getAnnotation(property, JsonSerialize.class);
            if (jsonSerialize != null) {
                if (!jsonSerialize.using().equals(JsonSerializer.None.class)) {
                    try {
                        return jsonSerialize.using().newInstance();
                    } catch (InstantiationException | IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return null;
        }

        private Class<?>[] getInterfaces(Class cls) {
            return cls.getInterfaces();
        }

        private List<AccessibleProperty> getAccessibleProperties(Class cls) {
            return memoizer.accessibleProperty(cls, () -> {
                // Gather all fields and methods
                Map<String, AccessibleProperty> accessibleProperties = new LinkedHashMap<>();
                Predicate<Field> shouldProcessField = fieldVisibilityAllowed(cls);
                Predicate<Method> shouldProcessMethod = getterVisibilityAllowed(cls);
                Predicate<Object> visible = (o) -> {
                    if (o instanceof Field) {
                        return shouldProcessField.test((Field) o);
                    } else if (o instanceof Method) {
                        return shouldProcessMethod.test((Method) o);
                    } else {
                        throw new RuntimeException("Could not process property of type " + o.getClass());
                    }
                };

                getDeclaredFields(cls).stream()
                        .map(f -> new AccessibleProperty(f.getName(), f.getAnnotations(), f))
                        .forEach(p -> accessibleProperties.put(p.name, p));
                getDeclaredMethods(cls).stream()
                        .filter(m -> m.getName().startsWith("get") && !m.getReturnType().equals(Void.class) && m.getParameters().length == 0)
                        .map(m -> new AccessibleProperty(getFieldNameFromGetter(m), m.getAnnotations(), m))
                        .forEach(p -> {
                            AccessibleProperty field = accessibleProperties.get(p.name);

                            // Combine annotations from the getter and the field
                            if (field != null) {
                                Set<Annotation> annotations = new HashSet<Annotation>(asList(field.annotations));
                                annotations.addAll(asList(p.annotations));
                                p = new AccessibleProperty(p.name, annotations.toArray(new Annotation[0]), p.property);
                            }

                            // TODO: Makes sure combined annotations are applied to the field when method visibility is disallowed
                            if (shouldProcessMethod.test((Method) p.property)) {
                                accessibleProperties.put(p.name, p);
                            }
                        });

                return accessibleProperties.values().stream()
                        .filter(p -> visible.test(p.property))
                        .collect(Collectors.toList());
            });
        }

        private List<Field> getDeclaredFields(Class cls) {
            List<Field> fields = new ArrayList<>();
            Stack<Class> parents = new Stack<>();
            parents.push(cls);

            while (!parents.isEmpty()) {
                Class c = parents.pop();

                Stream.of(c.getDeclaredFields()).forEach(f -> fields.add(f));

                if (c.getSuperclass() != null && !c.getSuperclass().equals(Object.class)) {
                    parents.push(c.getSuperclass());
                }
            }

            return fields;
        }

        private List<Method> getDeclaredMethods(Class cls) {
            List<Method> methods = new ArrayList<>();
            Stack<Class> parents = new Stack<>();
            parents.push(cls);

            while (!parents.isEmpty()) {
                Class c = parents.pop();

                Stream.of(c.getDeclaredMethods()).forEach(m -> methods.add(m));

                if (c.getSuperclass() != null && !c.getSuperclass().equals(Object.class)) {
                    parents.push(c.getSuperclass());
                }

                if (c.getInterfaces() != null) {
                    Stream.of(c.getInterfaces()).forEach(i -> parents.push(i));
                }
            }

            return methods;
        }

        private Annotation[] getAnnotations(Class cls) {
            return memoizer.annotations(cls, () -> {
                return cls.getAnnotations();
            });
        }

        @SuppressWarnings("unchecked")
        private <T extends Annotation> T getAnnotation(Class cls, Class<T> annotation) {
            Annotation[] annotations = getAnnotations(cls);
            if (annotations != null) {
                for (Annotation a : annotations) {
                    if (a.annotationType().equals(annotation)) {
                        return (T) a;
                    }
                }
            }
            return null;
        }

        @SuppressWarnings("unchecked")
        private <T extends Annotation> T getAnnotation(AccessibleProperty property, Class<T> annotation) {
            if (property.annotations != null) {
                for (Annotation a : property.annotations) {
                    if (a.annotationType().equals(annotation)) {
                        return (T) a;
                    }
                }
            }
            return null;
        }

        private Predicate<Field> fieldVisibilityAllowed(Class cls) {
            JsonAutoDetect autoDetect = getAnnotation(cls, JsonAutoDetect.class);

            if (autoDetect == null) {
                return f -> false;
            } else {
                switch (autoDetect.fieldVisibility()) {
                    case ANY:
                        return f -> true;
                    case PUBLIC_ONLY:
                        return f -> Modifier.isPublic(f.getModifiers());
                    case PROTECTED_AND_PUBLIC:
                        return f -> Modifier.isPublic(f.getModifiers()) || Modifier.isProtected(f.getModifiers());
                    case NON_PRIVATE:
                        return f -> !Modifier.isPrivate(f.getModifiers());
                    case DEFAULT:
                    case NONE:
                        return f -> false;
                    default:
                        throw new RuntimeException("No support for field visibility " + autoDetect.fieldVisibility());
                }
            }
        }

        private Predicate<Method> getterVisibilityAllowed(Class cls) {
            JsonAutoDetect autoDetect = getAnnotation(cls, JsonAutoDetect.class);

            if (autoDetect == null) {
                return m -> true;
            } else {
                switch (autoDetect.getterVisibility()) {
                    case DEFAULT:
                    case ANY:
                        return m -> true;
                    case PUBLIC_ONLY:
                        return m -> Modifier.isPublic(m.getModifiers());
                    case PROTECTED_AND_PUBLIC:
                        return m -> Modifier.isPublic(m.getModifiers()) || Modifier.isProtected(m.getModifiers());
                    case NON_PRIVATE:
                        return m -> !Modifier.isPrivate(m.getModifiers());
                    case NONE:
                        return m -> false;
                    default:
                        throw new RuntimeException("No support for field visibility " + autoDetect.fieldVisibility());
                }
            }
        }

        private String getFieldName(AccessibleProperty property) {
            JsonProperty jsonProperty = getAnnotation(property, JsonProperty.class);
            if (jsonProperty != null && jsonProperty.value().length() > 0) {
                return jsonProperty.value();
            } else {
                return property.name;
            }
        }

        private String getFieldNameFromGetter(Method method) {
            if (method.getName().equals("get")) {
                return method.getName();
            }
            String name = method.getName().replaceFirst("get", "");
            return name.substring(0, 1).toLowerCase() + name.substring(1);
        }

    }
}
